#!/usr/bin/python

"""
Pacmon, an updates notification tray app for the users of Arch Linux
Copyright 2006 Matt McDonald <metzen@gmail.com>

This file is part of Pacmon.

Pacmon is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

Pacmon is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
"""

__version__ = "0.7"


import os
import sys
import re
import time
import thread
from threading import Thread
import mutex
import subprocess

import pango
import pygtk
pygtk.require("2.0")
import gtk
import gtk.glade
import gobject
from ConfigParser import *
import vte

import parser
from messages import MessageQueue
try:
    import popup
    HAVE_NOTIFY = True
except:
    HAVE_NOTIFY = False
    pass

POPUP_MSGS = 6
CONFIG_PATH = os.path.expanduser('~/.config/pacmon.conf')
INTERVAL = '25'

class PacmonGUI(object):
    def __init__(self, DATA_DIR):
        self.DATA_DIR = DATA_DIR
        self.updateTag = 0
        self.refreshPkgs = False
        self.myMutex = mutex.mutex()
        self.message_queue = MessageQueue(True)
        GLADEFILE = DATA_DIR + '/glade/pacmon.glade'
        self.xml = gtk.glade.XML(GLADEFILE)

        self.init_trayicon()
        self.init_widgets()
        self.init_popup()
        self.init_menu()
        self.init_signal_handlers()
        self.init_outputbox()
        self.init_table()

        self.init_config()

    def init_config(self):
        self.conf = ConfigParser()
        files_read = self.conf.read(CONFIG_PATH)
        if len(files_read) == 0:
            print 'Creating config file at %s' % (CONFIG_PATH)
            self.conf.add_section('options')
            self.conf.set('options', 'check-interval', INTERVAL)
            self.conf.set('options', 'upgrade-cmd', 'xterm -e \'bash -c \"sudo pacman -Syu ; echo -n :: Press \[Enter\] to close. :: && read\"\'')
            self.conf.set('options', 'use-custom-upgrade-cmd', 'False')
            outfile = file(CONFIG_PATH, 'w')
            self.conf.write(outfile)
            outfile.close()
        self.updateIntervalSpin.set_value(int(self.conf.get('options', 'check-interval')))
        self.upgradeCmdEntry.set_text(self.conf.get('options', 'upgrade-cmd'))
        if(self.conf.get('options', 'use-custom-upgrade-cmd') == 'False'):
            self.builtinRadioBtn.set_active(True)
        else:
            self.customRadioBtn.set_active(True)

    def init_trayicon(self):
        self.trayicon = gtk.status_icon_new_from_file(self.DATA_DIR +\
                                                      '/images/pacman-open.png')
        self.trayicon.set_visible(False)
        self.trayicon.connect('activate', self.showUpdates)
        self.trayicon.connect('popup-menu', self.popupMenu)

    def init_menu(self):
        checkUpdatesMenuItem = gtk.ImageMenuItem('Check for Updates')
        checkImg = gtk.image_new_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU)
        checkUpdatesMenuItem.set_image(checkImg)
        checkUpdatesMenuItem.connect('activate', self.update)

        showUpdatesMenuItem = gtk.ImageMenuItem('Show Updates')
        showImg = gtk.image_new_from_icon_name("system-software-update", gtk.ICON_SIZE_MENU)
        showUpdatesMenuItem.set_image(showImg)
        showUpdatesMenuItem.connect('activate', self.showUpdates)

        installUpdatesMenuItem = gtk.ImageMenuItem('Install Updates')
        installImg = gtk.image_new_from_stock(gtk.STOCK_SAVE, gtk.ICON_SIZE_MENU)
        installUpdatesMenuItem.set_image(installImg)
        installUpdatesMenuItem.connect('activate', self.upgrade)

        preferences = gtk.ImageMenuItem(gtk.STOCK_PREFERENCES)
        preferences.connect('activate', self.showPreferences)

        quit = gtk.ImageMenuItem(gtk.STOCK_QUIT)
        quit.connect('activate', self.quit)

        separator1 = gtk.SeparatorMenuItem()
        separator2 = gtk.SeparatorMenuItem()

        self.menu = gtk.Menu()
        self.menu.append(checkUpdatesMenuItem)
        self.menu.append(showUpdatesMenuItem)
        self.menu.append(installUpdatesMenuItem)
        self.menu.append(separator1)
        self.menu.append(preferences)
        self.menu.append(separator2)
        self.menu.append(quit)
        self.menu.show_all()

    def init_popup(self):
        if HAVE_NOTIFY:
            self.popup = popup.PacmonNotification('Updates Available!',
                                                  self.trayicon, self.showUpdates,
                                                  self.upgrade)

    def init_signal_handlers(self):
        sig = {"on_prefCloseButton_clicked"           : self.prefWindowClose,
               "on_updateIntervalSpin_value_changed"  : self.prefSpinnerChanged,
               "on_window_destroy"                    : self.destroy_decoy,
               "on_updateRefreshButton_clicked"       : self.update,
               "on_applyUpdatesButton_clicked"        : self.upgrade,
               "on_progDialogCloseBtn_clicked"        : self.progDialogClose,
               "on_upgrade_cmd_group_changed"         : self.upgradeCmdGroupChanged}
        self.xml.signal_autoconnect(sig)

    def init_widgets(self):
        self.prefWindow = self.xml.get_widget("preferencesWindow")
        self.prefCloseButton = self.xml.get_widget("prefCloseButton")
        self.updateIntervalSpin = self.xml.get_widget("updateIntervalSpin")
        self.upgradeCmdEntry = self.xml.get_widget("upgradeCmdEntry")
        self.updateWindow = self.xml.get_widget("updateWindow")
        self.packageListView = self.xml.get_widget("packageList")
        self.updateRefreshButton = self.xml.get_widget("updateRefreshButton")
        self.applyUpdatesButton = self.xml.get_widget("applyUpdatesButton")
        #self.applyUpdatesButton.hide()
        self.pacmanOutputBox = self.xml.get_widget("pacmanOutputBox")
        self.progressDialog = self.xml.get_widget("progressDialog")
        self.pacProgressBar = self.xml.get_widget("pacProgressBar")
        self.progressLabel = self.xml.get_widget("progressLabel")
        self.outputExpander = self.xml.get_widget("outputExpander")
        self.outputExpander.connect("notify::expanded", self.outputExpanderClicked)
        self.outputScroll = self.xml.get_widget("outputScroll")
        self.autocloseCheckButton = self.xml.get_widget("autocloseCheckButton")
        self.progDialogCloseBtn = self.xml.get_widget("progDialogCloseBtn")
        self.builtinRadioBtn = self.xml.get_widget("builtin_radio_button")
        self.customRadioBtn = self.xml.get_widget("custom_radio_button")

    def init_outputbox(self):
        self.pacmanOutputBox.modify_font(pango.FontDescription("monospace 9"))
        self.pacmanOutputBox.modify_base(gtk.STATE_NORMAL,\
                                         gtk.gdk.color_parse("black"))
        self.pacmanOutputBox.modify_text(gtk.STATE_NORMAL,\
                                         gtk.gdk.color_parse("white"))

    def init_table(self):
        self.packageListStore = gtk.ListStore(gobject.TYPE_STRING,\
                                              gobject.TYPE_STRING,\
                                              gobject.TYPE_STRING)
        renderer1 = gtk.CellRendererText()
        renderer2 = gtk.CellRendererText()
        renderer3 = gtk.CellRendererText()
        column1 = gtk.TreeViewColumn("Package", renderer1, text=0)
        column2 = gtk.TreeViewColumn("Installed Version", renderer2, text=1)
        column3 = gtk.TreeViewColumn("Updated Version", renderer3, text=2)
        self.packageListView.append_column(column1)
        self.packageListView.append_column(column2)
        self.packageListView.append_column(column3)
        self.packageListView.set_model(self.packageListStore)


#-----callback handlers--------------------------------------------------------

    def showPreferences(self, obj):
        self.prefWindow.show()

    def showUpdates(self, obj):
        self.updateWindow.show()

    def popupMenu(self, obj, button, time):
        self.menu.popup(None, None, None, button, time)

    def trayClicked(self, obj, event):
        if event.button == 1:
            self.updateWindow.show()
        if event.button == 3:
            self.menu.popup(None, None, None, event.button, event.time)
        pass # bring up right click

    def quit(self, obj):
        gtk.main_quit()

    def prefWindowClose(self, obj):
        # close the config window and Save the settings to disk
        self.prefWindow.hide()
        check_interval = str(int(self.updateIntervalSpin.get_value()))
        use_custom = str(self.customRadioBtn.get_property('active'))
        self.conf.set('options', 'upgrade-cmd', self.upgradeCmdEntry.get_text())
        self.conf.set('options', 'check-interval', check_interval)
        self.conf.set('options', 'use-custom-upgrade-cmd', use_custom)  
        print "Saving settings...",
        outfile = file(CONFIG_PATH, 'w')
        self.conf.write(outfile)
        outfile.close()
        print "done."

    def prefSpinnerChanged(self, obj):
        timeout = int(self.updateIntervalSpin.get_value())
        self.conf.set('options', 'check-interval', str(timeout))
        #remove old update and replace with new one with new timeout
        if not self.updateTag == 0:
            gobject.source_remove(self.updateTag)
            self.updateTag = gobject.timeout_add(timeout * 60 * 1000, 
                self.update, True)

    def progDialogClose(self, obj):
        self.progressDialog.hide()
        if self.refreshPkgs:
            #Updater().start()
            self.refreshPkgs = False
        self.outputExpander.set_expanded(False)

    def destroy_decoy(self, obj, data=None):
        obj.hide()
        return True

    def outputExpanderClicked(self, obj, event):
        # TODO: this is reallllly sloppy, gotta figure out the right way to do
        # this; mabey listen for a hide event on the textbox or something
        def resizeProgressDialog():
            # collapse window when expander is set to hide contents
            if self.outputExpander.get_expanded() == False:
                time.sleep(.2) # wait for expander close animation, then resize
                gtk.gdk.threads_enter()
                self.progressDialog.resize(1,1)
                gtk.gdk.threads_leave()
        thread.start_new_thread(resizeProgressDialog, ())

    def upgradeCmdGroupChanged(self, obj):
        if self.builtinRadioBtn.get_property("active"):
            self.upgradeCmdEntry.set_sensitive(False)
        else:
            self.upgradeCmdEntry.set_sensitive(True)
    
    
#-------workhorse functions----------------------------------------------------

    def update_progress_dialog(self, get_progress, message_queue):
        """Update the progress dialog to reflect the current status of an action

        get_progress -- a function to call that will return a float in range [0,1]
        message_queue -- the message queue object, msgs to output will be stored here
        """

        # Update the progressbar to the percent indicated by calling the
        # 'get_progress' fuction
        self.update_progressbar(get_progress())
        self.update_outputbox(message_queue)

        # if progress is 100% and we've read all the messages on the queue
        if get_progress() == 1 and message_queue.length() == 0:
            pacmanBuffer = self.pacmanOutputBox.get_buffer()
            endI = pacmanBuffer.get_end_iter()
            mark = pacmanBuffer.create_mark("endMark", endI,True)
            pacmanBuffer.move_mark(mark,endI)
            pacmanBuffer.insert(endI, "DONE!")
            self.pacmanOutputBox.scroll_to_mark(mark,.1)
            return False # we are done, so pass False back to gobject.timeout_add
        else:
            return True # not done yet, pass True back to gobject.timeout_add so we keep getting called

    def update_progressbar(self, fraction):
        """ Update the progressbar to the specified fraction"""
        self.pacProgressBar.set_fraction(fraction)

    def update_outputbox(self, message_queue):
        for msg in message_queue.read_messages():
            pacmanBuffer = self.pacmanOutputBox.get_buffer()
            endI = pacmanBuffer.get_end_iter()
            mark = pacmanBuffer.create_mark("endMark", endI,True)
            pacmanBuffer.move_mark(mark,endI)
            pacmanBuffer.insert(endI, msg)
            self.pacmanOutputBox.scroll_to_mark(mark,.1)

    def update(self, bool):
        thread.start_new_thread(self.update_thread, ())
        return bool # we are called by gobject so we can return True to call again

    def update_thread(self):
        def do(junk):
            if self.updateWindow.get_property('visible'):
                self.packageListStore.clear()
                gtk.gdk.threads_enter()
                self.progDialogCloseBtn.set_sensitive(False)
                self.progressLabel.set_markup("<big><b>Checking for "+\
                    "Updates</b></big>\nPackage repositories are being "+\
                    "searched for updated packages")
                self.pacmanBuffer = self.pacmanOutputBox.get_buffer()
                self.pacmanBuffer.set_text("")
                self.pacProgressBar.set_fraction(0)
                self.outputExpander.set_expanded(False)
                self.progressDialog.resize(1,1)
                self.progressDialog.show()
                gtk.gdk.threads_leave()

            gtk.gdk.threads_enter()
            self.trayicon.set_tooltip('Checking for updates...')

            # gtk.StatusIcon doesn't seem to support gif animations
            #self.trayicon.set_from_file(self.DATA_DIR + '/images/pacman-ani.gif')

            #packageListView.set_model(None)  #detach model from view for faster update
            gtk.gdk.threads_leave()

            # set our progress dialog to poll for updates from the parser
            gobject.timeout_add(100, self.update_progress_dialog,
                                parser.get_progress, self.message_queue)
            updatesList = parser.get_updates_list(self.message_queue)
            pkgs = ""
            if len(updatesList) != 0:
                # we have some updates, so set the liststore and tooltip
                self.packageListStore.clear()
                count = 0
                for package in updatesList:
                    self.packageListStore.append(package)
                    if count == 0:
                        pkgs += package[0] + " " + package[2]
                    elif count < POPUP_MSGS:
                        pkgs += "\n" + package[0] + " " + package[2]
                    count += 1
                if len(updatesList) > POPUP_MSGS:
                    pkgs += "\n..."

                gtk.gdk.threads_enter()
                #self.trayicon.show_all()
                self.trayicon.set_visible(True)
                gtk.gdk.threads_leave()

                # and show a notification if we have libnotify
                if HAVE_NOTIFY:
                    self.popup.show_popup(pkgs)
                pkgs = "Updates available!\n" + pkgs
                self.trayicon.set_tooltip(pkgs)
            else:
                # no updates to show
                self.trayicon.set_visible(False)
                self.packageListStore.clear()

            gtk.gdk.threads_enter()
            self.trayicon.set_from_file(self.DATA_DIR + '/images/pacman-open.png')
            gtk.gdk.threads_leave()
            #packageListView.set_model(packageListStore) #reattach the model

            if self.updateWindow.get_property('visible'):
                gtk.gdk.threads_enter()
                #pacmanBuffer = pacmanOutputBox.get_buffer()
                #pacmanBuffer.set_text("DONE!")
                #pacProgressBar.set_fraction(1)
                if self.autocloseCheckButton.get_active():
                    self.progressDialog.hide()
                self.progDialogCloseBtn.set_sensitive(True)
                gtk.gdk.threads_leave()

            self.myMutex.unlock()
        self.myMutex.lock(do, None)


    def upgrade(self, obj):
        thread.start_new_thread(self.upgrade_thread, ())

    def upgrade_thread(self):
        def do(junk):
            gtk.gdk.threads_enter()
            self.updateWindow.hide()
            gtk.gdk.threads_leave()
            
            if (self.conf.get('options', 'use-custom-upgrade-cmd') == 'True'):
                #---------CUSTOM SUBPROCESS STYLE------------
                subprocess.call(self.conf.get('options', 'upgrade-cmd'), shell=True)
                self.trayicon.set_visible(False)
                self.myMutex.unlock()
                self.update(False)
            
            else:
                #-------------VTE TERMINAL STYLE-------------
                term = vte.Terminal()
                term.set_scrollback_lines(8192)
                vscroll = gtk.VScrollbar()
                vscroll.set_adjustment(term.get_adjustment())
                hbox = gtk.HBox()
                hbox.pack_start(term, expand=True)
                hbox.pack_end(vscroll, expand=False)
                term_win = gtk.Window()
                term_win.set_title("Installing Updates - Pacmon")
                term_win.add(hbox)
                gtk.gdk.threads_enter()
                term_win.show_all()
                gtk.gdk.threads_leave()
                def child_exit(obj):
                    def close_term(obj, event):
                        term_win.hide()
                        self.trayicon.set_visible(False)
                        # update after we have upgraded to reflect the accurate
                        # state of the system
                        self.update(False)
                    term.connect('key-press-event', close_term)
                    term.feed(':: Finished. Press any key to close ::')
                    self.myMutex.unlock()
                    
                term.connect('child-exited', child_exit)
                term.feed("Enter the root ") # " Password:"  is filled in by call
                term.fork_command('su', ['su','-c','pacman -Syu'])
                
        self.myMutex.lock(do, None)


#--------run-------------------------------------------------------------------
    def run(self):
        gtk.gdk.threads_init()

        timeout = int(self.conf.get('options', 'check-interval'))
        # timeout * (60 sec/min) * (1000 milliseconds/sec)
        updateTag = gobject.timeout_add(timeout * 60 * 1000, self.update, True)
        gobject.idle_add(self.update, False)

        gtk.main()

